# -*- coding: iso-8859-15 -*-
#
# LICENSE:
# 
# Copyright 2008 Håkan Lindqvist. All rights reserved.
# 
# Redistribution and use in source and binary forms, 
# with or without modification, are permitted provided 
# that the following conditions are met:
# 
#    1. Redistributions of source code must retain the 
#       above copyright notice, this list of conditions 
#       and the following disclaimer.
#    2. Redistributions in binary form must reproduce the 
#       above copyright notice, this list of conditions 
#       and the following disclaimer in the documentation 
#       and/or other materials provided with the distribution.
# 
# THIS SOFTWARE IS PROVIDED BY HÅKAN LINDQVIST ``AS IS'' 
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 
# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL HÅKAN LINDQVIST 
# OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR 
# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 
# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 
# DAMAGE.
# 

import os.path
import sys

from datetime import datetime

from reportlab.lib.colors               import Color               as rlColor
from reportlab.lib.enums                import TA_CENTER           as rlTA_CENTER
from reportlab.lib.enums                import TA_LEFT             as rlTA_LEFT
from reportlab.lib.pagesizes            import A4                  as rlA4
from reportlab.lib.styles               import ParagraphStyle      as rlParagraphStyle
from reportlab.lib.styles               import getSampleStyleSheet as rlGetSampleStyleSheet
from reportlab.lib.styles               import getSampleStyleSheet as rlStyles
from reportlab.lib.units                import cm                  as rlcm
from reportlab.pdfgen                   import canvas              as rlCanvas
from reportlab.platypus                 import PageBreak           as rlPageBreak
from reportlab.platypus                 import Paragraph           as rlParagraph
from reportlab.platypus                 import SimpleDocTemplate   as rlSimpleDocTemplate
from reportlab.platypus                 import Spacer              as rlSpacer
from reportlab.platypus.doctemplate     import BaseDocTemplate     as rlBaseDocTemplate
from reportlab.platypus.doctemplate     import PageTemplate        as rlPageTemplate
from reportlab.platypus.doctemplate     import _doNothing          as rl_doNothing
from reportlab.platypus.frames          import Frame               as rlFrame
from reportlab.platypus.tableofcontents import TableOfContents     as rlTableOfContents
from reportlab.platypus.tables          import TableStyle          as rlTableStyle
from reportlab.rl_config                import defaultPageSize     as rlDefaultPageSize

#################################################
# Functions used for formatting report pages
def rlTitlePageFormatter(canvas, doc):
	pass

def rlContentPageFormatter(canvas, doc):
	canvas.saveState()
	canvas.drawCentredString(rlDefaultPageSize[0]/2, 0.75 * rlcm, "%d" % doc.page)
	canvas.restoreState()

#################################################
# Report utility class
class VisitDocTemplate(rlSimpleDocTemplate):
	"""The document template used for all PDF documents to get the
	   table of contents to build correctly and still be able to use
	   custom formatters for the first and later pages.
	   It is an extension to the standard platypus template, which
	   fails to correctly render index pages.
	"""

	def afterFlowable(self, flowable):
		"""Registers TOC entries and makes outline entries."""

		if flowable.__class__.__name__ == 'Paragraph':
			styleName = flowable.style.name

			# Register TOC entries that are [hH]eadingN
			if styleName.lower()[:7] == 'heading':
				level   = int(styleName[-1]) - 1
				text    = flowable.getPlainText()
				pageNum = self.page
				self.notify('TOCEntry', (level, text, pageNum))

				# Add PDF outline entries (not really needed/tested here).
				key = str(hash(flowable))
				c   = self.canv
				c.bookmarkPage(key)
				c.addOutlineEntry(text, key, level=level, closed=0)

	def multiBuild(self,flowables,onFirstPage=rl_doNothing, onLaterPages=rl_doNothing, canvasmaker=rlCanvas.Canvas):
		"""Build the document using the flowables. Annotate the first 
		   page using the onFirstPage function and later pages using 
		   the onLaterPages function. The onXXX pages should follow the 
		   signature.
	
		   def myOnFirstPage(canvas, document):
		   # do annotations and modify the document
		   ...
	
		   The functions can do things like draw logos, page numbers,
		   footers, etc. They can use external variables to vary the 
		   look (for example providing page numbering or section names).
		"""
		self._calc() #in case we changed margins sizes etc
	
		frameT = rlFrame(self.leftMargin, self.bottomMargin, self.width, self.height, id='normal')
	
		self.addPageTemplates([rlPageTemplate(id='First', frames=frameT, onPage=onFirstPage, pagesize=self.pagesize), rlPageTemplate(id='Later', frames=frameT, onPage=onLaterPages, pagesize=self.pagesize)])
	
		if onFirstPage is rl_doNothing and hasattr(self,'onFirstPage'):
			self.pageTemplates[0].beforeDrawPage = self.onFirstPage
	
		if onLaterPages is rl_doNothing and hasattr(self,'onLaterPages'):
			self.pageTemplates[1].beforeDrawPage = self.onLaterPages
	
		rlBaseDocTemplate.multiBuild(self,flowables, canvasmaker=canvasmaker) 

#################################################
# Report classes
class ReportException(Exception):
	def __init__(self, message):
		Exception.__init__(self, message)


class Renderable():
	"""Master class for all Report tokens, it should be extended
	   by all classes that are related to a report.
	"""

	__HTML__     = "text/html"
	__PDF__      = "application/pdf"
	__PDFSTYLE__ = rlGetSampleStyleSheet()

	def __renderhtml__(self):
		"""Get a HTML representation of the report."""
		raise ReportException("HTML renderer not implemented.")

	def __renderpdf__(self):
		"""Get a PDF representation of the report."""
		raise ReportException("PDF renderer not implemented.")

	def getId(self):
		"""Get a unique identifier, based on time."""
		dt = datetime.now()
		return "".join([str(dt.hour), str(dt.minute), str(dt.second), str(dt.microsecond)])
	
	def render(self, mimetype):
		"""Render the token in the type specified.
		   The method must be overriden in subclasses, and should then
		   return the approriate (list of) objects.

		   A subclass needs to implement the __renderhtml__ and the
		   __renderpdf__ methods.
		"""
		if   mimetype == self.__HTML__:
			return self.__renderhtml__()
		elif mimetype == self.__PDF__:
			return self.__renderpdf__()
		else:
			raise ReportException("Unsupported MIME type: " + mimetype)

	def stylecode(self, mimetype, style):
		if   mimetype == self.__HTML__:
			return style
		elif mimetype == self.__PDF__:
			return self.__PDFSTYLE__[style]
		else:
			raise ReportException("Unsupported MIME type: " + mimetype)


class Report(Renderable):
	"""Report base class."""

	def __init__(self, out=sys.stdout):
		self.__out   = out
		self.__story = []

	def add(self, renderable):
		"""Add a token to the report."""
		self.__story.append(renderable)

	def __renderhtml__(self):
		HTMLReport(self.__out, self.__story).render()

	def __renderpdf__(self):
		PDFReport(self.__out, self.__story).render()


class HTMLReport(Report):
	"""HTML report class, normally constructed during rendering by the base class."""

	def __init__(self, out, story=None):
		if story:
			self.__story = story

		self.__out = out

	def render(self, mimetype=None):
		if mimetype and not mimetype == self.__HTML__:
			raise ReportException("HTMLReport only supports rendering of HTML reports, a report of type '%s' was requested")

		self.__renderhtml__()

	def __renderhtml__(self):
		top = """
		<html>
		<head>
		  <title> %s </title>
		  <style type="text/css">
		    html,body  { border:0px; margin:0px; padding:0px; }
		    body       { background: #dedede; border:0px; margin:10px; }

		    a:active   { color:black;   }
		    a:hover    { color:red;     }
		    a:link     { color:red;     }
		    a:visited  { color:black;   }

		    #Title     { background:#fefefe; color:red; border-width: 1px; border-bottom-style: solid; font-size: x-large; font-weight: bold; }
		    #Abstract  { background:#ffd;        }
		    #Abstract .title { font-weight:bold; }

			#Index           { background:#ffd;  }
			#Index .title    { font-weight:bold; }
			#Index .entries  { font-size:small; margin-left: 40px; }
		    #Index a:active  { color:black; }
		    #Index a:hover   { color:black; }
		    #Index a:link    { color:black; }
		    #Index a:visited { color:black; }

		    #content   { background:#fff; border:1px; border-top: 2px; border-bottom: 2px; border-style: solid; border-color:black; padding:10px; padding-bottom:0px; margin-bottom:0px; }
			#content .Heading1 { font-size: large;  font-weight:bold; }
			#content .Heading2 { font-size: normal; font-weight:bold; }
			#content .Heading3 { font-size: small;  font-weight:bold; font-style:italic; }

		    #Separator { color:#fefefe; }
		    #Separator hr { background:#ee0000; border:0px; color:#ee0000; height:1px; }
		  </style>
		</head>
		<body>
		<div id="content">
		""" % "Report"

		bottom = """
		</div>
		</body>
		</html>
		"""

		report = []

		for item in self.__story:
			if isinstance(item, ReportIndex):
				report += item.getIndex(self.__HTML__, self.__story) # index needs special treatment
			else:
				report += item.__renderhtml__()

		report = '\n'.join([top, ''.join(report), bottom])

		if not self.__out == sys.stdout:
			if not self.__out.endswith(".html"):
				self.__out += ".html"

			out = file(self.__out, "w")
			out.write(report)
		else:
			print report


class PDFReport(Report):
	"""PDF report class, normally constructed during rendering by the base class."""

	def __init__(self, out, story=None):
		if story:
			self.__story = story

		self.__out = out

	def render(self, mimetype=None):
		if mimetype and not mimetype == self.__PDF__:
			raise ReportException("PDFReport only supports rendering of PDF reports, a report of type '%s' was requested")

		self.__renderpdf__()

	def __renderpdf__(self):
		if not self.__out == sys.stdout:
			if not self.__out.endswith(".pdf"):
				self.__out += ".pdf"

		report = []

		for item in self.__story:
			if isinstance(item, ReportIndex):
				report += item.getIndex(self.__PDF__, self.__story) # index needs special treatment
			else:
				report += item.__renderpdf__()

		document = VisitDocTemplate(self.__out, pagesize=rlA4)
		document.multiBuild(report, onFirstPage=rlTitlePageFormatter, onLaterPages=rlContentPageFormatter)


class ReportIndex(Renderable):
	"""An index to all Headings in the report."""

	def getIndex(self, mimetype, contents):
		if   mimetype == self.__HTML__:
			return self.__renderhtml__(contents)
		elif mimetype == self.__PDF__:
			return self.__renderpdf__(contents)
		else:
			raise ReportException("Unsupported MIME type: " + mimetype)

	def __renderhtml__(self, contents=None):
		if not contents:
			raise ReportException("A ReportIndex needs informaton about Report's, call the getIndex method.")

		links = []

		for item in contents:
			if isinstance(item, Heading):
				links.append("""
				<a href="#%s"> %s </a>
				""" % (item.getIdentifier(), item.getText()))

		index = """
		<div id="Index">
		  <p class="title">Index:</p>
		  <p class="entries">
		    %s
		  </p>
		</div>

		<div id="Separator">
		  <hr/>
		</div>
		""" % "<br/>".join(links)

		return [index]
	
	def __renderpdf__(self, contents=None):
		if not contents:
			raise ReportException("A ReportIndex needs informaton about Report's, call the getIndex method.")

		toc            = rlTableOfContents()
		toc.tableStyle = rlTableStyle([('VALIGN', (0,0), (-1,-1), 'TOP'), ('BACKGROUND',(0,0),(-1,-1),rlColor(0.95,0.95,0.95)), ('FONT', (0,0),(-1,-1),'Times-Roman', 10)])

		return [toc] + PageBreak().__renderpdf__()


class ReportTitle(Renderable):
	"""Report title page."""

	def __init__(self, title=None, titleColor=None, abstract=None):
		if not title:
			raise Exception("The 'title' parameter must contain a value.")

		self.__title__      = title
		self.__titleColor__ = titleColor
		self.__abstract__   = abstract

	def __renderhtml__(self):
		title = """
		<div id="Title">
		  %s
		</div>
		""" % self.__title__

		if self.__abstract__:
			title += """
			<div id="Abstract">
			<p class="title">Abstract:</p>
			<p>
			  %s
			</p>
			</div>
			""" % self.__abstract__

		title += """
		<div id="Separator">
		  <hr/>
		</div>
		"""

		return [title]

	def __renderpdf__(self):
		top_space   = rlDefaultPageSize[1]/5
		after_space = rlDefaultPageSize[1]/2.5
		titleColor  = self.__titleColor__ or rlColor(1,0,0)

		titlestyle  = rlParagraphStyle(name='TitleHeader', alignment=rlTA_CENTER, fontName='Helvetica', fontSize=52, textColor=titleColor)

		ret = []

		ret.append(rlSpacer(0, top_space))
		ret.append(rlParagraph(self.__title__, titlestyle))

		if self.__abstract__:
			abstractstyle = rlParagraphStyle(name='AbstractStyle', fontSize=10, alignment=rlTA_CENTER, backColor=rlColor(0.95,0.95,0.95), borderWidth=0, borderColor=rlColor(0,0,0), borderPadding=8, bulletIndent=50)
			ret.append(rlSpacer(0, after_space))
			ret.append(rlParagraph(self.__abstract__, abstractstyle))

		ret.append(rlPageBreak())

		return ret

class PageBreak(Renderable):
	"""A token that can be added to a report to get a new page."""

	def __renderhtml__(self):
		pagebreak = """
		<div id="Pagebreak">
		  <hr/>
		</div>
		"""
		return [pagebreak]

	def __renderpdf__(self):
		return [rlPageBreak()]
	

class Heading(Renderable):
	"""A token that delimits a text set in a Report."""
	
	def __init__(self, text, level):
		if not level in [1,2,3]:
			raise ReportException("Levels must be either of 1, 2 or 3.")

		self.__text  = text
		self.__level = level
		self.__id    = self.getId()

	def getIdentifier(self):
		return self.__id
	
	def getLevel(self):
		return self.__level

	def getText(self):
		return self.__text

	def __renderhtml__(self):
		if isinstance(self.__text, Renderable):
			text = self.__text.render(self.__HTML__)
		else:
			text = self.__text

		heading = """
		<div id="%s" class="Heading%s">
			%s
		</div>
		""" % (self.__id, self.__level, text)

		return [heading]

	def __renderpdf__(self):
		if isinstance(self.__text, Renderable):
			text = self.__text.render(self.__PDF__)
		else:
			text = self.__text

		style = "Heading" + str(self.__level)

		return [rlParagraph(text, self.stylecode(self.__PDF__, style))]


class Paragraph(Renderable):
	"""A section of a report containing text, normally follows a Heading."""

	def __init__(self, text):
		self.__text = text

	def __renderhtml__(self):
		if isinstance(self.__text, Renderable):
			text = self.__text.render(self.__HTML__)
		else:
			text = self.__text

		paragraph = """
		<div class="Paragraph">
			%s
		</div>
		""" % text

		return [paragraph]

	def __renderpdf__(self):
		if isinstance(self.__text, Renderable):
			text = self.__text.render(self.__PDF__)
		else:
			text = self.__text

		style = "Normal"

		return [rlParagraph(text, self.stylecode(self.__PDF__, style))]


# If used as a script, run a small test case
if __name__ == "__main__":
	report   = Report(out="test")

	title    = ReportTitle(title='Foo test report', abstract='A simple test report with an abstract that is only there to occupy some text.')
	index    = ReportIndex()

	report.add(title)
	report.add(index)

	report.add(Heading(text="First1", level=1))
	report.add(Paragraph("bla bla bla"))
	report.add(PageBreak())

	report.render("text/html")
	report.render("application/pdf")

